/*
 * Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package org.graalvm.visualizer.search.ui;

import org.netbeans.api.annotations.common.StaticResource;
import org.netbeans.swing.outline.Outline;
import org.openide.awt.ToolbarWithOverflow;
import org.openide.explorer.ExplorerManager;
import org.openide.explorer.ExplorerUtils;
import org.openide.explorer.view.OutlineView;
import org.openide.explorer.view.Visualizer;
import org.openide.nodes.Node;
import org.openide.util.Exceptions;
import org.openide.util.ImageUtilities;
import org.openide.util.Lookup;
import org.openide.util.Mutex;
import org.openide.util.NbBundle;

import javax.swing.AbstractButton;
import javax.swing.ActionMap;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;
import javax.swing.text.DefaultEditorKit;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyVetoException;
import java.util.LinkedList;
import java.util.List;
import java.util.MissingResourceException;

/**
 * @author jhavlin
 */
@NbBundle.Messages({
        "ACTION_Stop=Stop search",
        "ACTION_ExpandCollapse=Expand/Collapse all nodes in tree",
        "ACTION_NextMatch=Next",
        "ACTION_PreviousMatch=Previous",
        "ACTION_SearchAgain=Search again..."

})
public abstract class AbstractSearchResultsPanelBase extends javax.swing.JPanel
        implements ExplorerManager.Provider, Lookup.Provider {

    @StaticResource
    private static final String REFRESH_ICON =
            "org/graalvm/visualizer/search/resources/refresh.png";              //NOI18N
    @StaticResource
    private static final String STOP_ICON =
            "org/graalvm/visualizer/search/resources/stop.png";                 //NOI18N
    @StaticResource
    private static final String NEXT_ICON =
            "org/graalvm/visualizer/search/resources/next.png";                 //NOI18N
    @StaticResource
    private static final String PREV_ICON =
            "org/graalvm/visualizer/search/resources/prev.png";                 //NOI18N
    @StaticResource
    private static final String EXPAND_ICON =
            "org/graalvm/visualizer/search/resources/expandTree.png";           //NOI18N
    @StaticResource
    private static final String COLLAPSE_ICON =
            "org/graalvm/visualizer/search/resources/collapseTree.png";         //NOI18N

    private ExplorerManager explorerManager;
    protected JButton btnStopRefresh = new JButton();
    protected JButton btnPrev = new JButton();
    protected JButton btnNext = new JButton();
    protected JToggleButton btnExpand = new JToggleButton();
    private Lookup lookup;
    private volatile boolean btnStopRefreshInRefreshMode = false;

    /**
     * Creates new form AbstractSearchResultsPanel
     */
    public AbstractSearchResultsPanelBase() {
        initComponents();
        explorerManager = new ExplorerManager();

        ActionMap map = this.getActionMap();
        // map delete key to delete action
        map.put("delete", //NOI18N
                ExplorerUtils.actionDelete(explorerManager, false));
        map.put(DefaultEditorKit.copyAction,
                ExplorerUtils.actionCopy(explorerManager));
        map.put(DefaultEditorKit.cutAction,
                ExplorerUtils.actionCut(explorerManager));

        lookup = ExplorerUtils.createLookup(explorerManager, map);
        initActions();
        initToolbar();
        initSelectionListeners();
    }

    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        jPanel1 = new javax.swing.JPanel();
        toolBar = new ToolbarWithOverflow();
        contentPanel = new javax.swing.JPanel();
        header = new javax.swing.JPanel();

        javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1);
        jPanel1.setLayout(jPanel1Layout);
        jPanel1Layout.setHorizontalGroup(
                jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                        .addGap(0, 100, Short.MAX_VALUE)
        );
        jPanel1Layout.setVerticalGroup(
                jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                        .addGap(0, 100, Short.MAX_VALUE)
        );

        setLayout(new java.awt.BorderLayout());

        toolBar.setFloatable(false);
        toolBar.setOrientation(JToolBar.VERTICAL);
        toolBar.setRollover(true);
        toolBar.setPreferredSize(null);
        toolBar.setRequestFocusEnabled(false);
        add(toolBar, java.awt.BorderLayout.WEST);

        contentPanel.setBorder(javax.swing.BorderFactory.createLineBorder(javax.swing.UIManager.getDefaults().getColor("controlShadow")));
        contentPanel.setLayout(new javax.swing.BoxLayout(contentPanel, javax.swing.BoxLayout.LINE_AXIS));
        add(contentPanel, java.awt.BorderLayout.CENTER);

        header.setLayout(new javax.swing.BoxLayout(header, javax.swing.BoxLayout.LINE_AXIS));
        add(header, java.awt.BorderLayout.NORTH);
    }// </editor-fold>//GEN-END:initComponents

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JPanel contentPanel;
    private javax.swing.JPanel header;
    private javax.swing.JPanel jPanel1;
    private javax.swing.JToolBar toolBar;
    // End of variables declaration//GEN-END:variables

    @Override
    public final ExplorerManager getExplorerManager() {
        return explorerManager;
    }

    protected JPanel getHeaderPane() {
        return header;
    }

    protected void initToolbar() {

        toolBar.setRollover(true);

        initStopRefreshButton();
        toolBar.add(btnStopRefresh);
        initPrevButton();
        toolBar.add(btnPrev);
        initNextButton();
        toolBar.add(btnNext);
        initExpandButton();
        toolBar.add(btnExpand);

        toolBar.setMinimumSize(new Dimension(
                (int) toolBar.getMinimumSize().getWidth(),
                (int) btnStopRefresh.getMinimumSize().getHeight()));
    }

    private void initStopRefreshButton() throws MissingResourceException {
        sizeButton(btnStopRefresh);
        btnStopRefresh.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (btnStopRefreshInRefreshMode) {
                    modifyCriteria();
                } else {
                    terminateSearch();
                }
            }
        });
        showStopButton();
    }

    protected void showStopButton() {
        btnStopRefreshInRefreshMode = false;
        btnStopRefresh.setToolTipText(Bundle.ACTION_Stop());
        btnStopRefresh.setIcon(ImageUtilities.loadImageIcon(STOP_ICON, true));
    }

    private void initExpandButton() {
        sizeButton(btnExpand);
        btnExpand.setIcon(ImageUtilities.loadImageIcon(EXPAND_ICON, true));
        btnExpand.setSelectedIcon(ImageUtilities.loadImageIcon(
                COLLAPSE_ICON, true));
        btnExpand.setToolTipText(Bundle.ACTION_ExpandCollapse());
        btnExpand.setEnabled(false);
        btnExpand.setSelected(false);
    }

    private void initNextButton() {
        sizeButton(btnNext);
        btnNext.setIcon(ImageUtilities.loadImageIcon(NEXT_ICON, true));
        btnNext.setToolTipText(Bundle.ACTION_NextMatch());
        btnNext.setEnabled(false);
        btnNext.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                shift(1);
            }
        });
    }

    private void initPrevButton() {
        sizeButton(btnPrev);
        btnPrev.setIcon(ImageUtilities.loadImageIcon(PREV_ICON, true));
        btnPrev.setToolTipText(Bundle.ACTION_PreviousMatch());
        btnPrev.setEnabled(false);
        btnPrev.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                shift(-1);
            }
        });
    }

    protected void sizeButton(AbstractButton button) {
        Dimension dim = new Dimension(24, 24);
        button.setMinimumSize(dim);
        button.setMaximumSize(dim);
        button.setPreferredSize(dim);
    }

    protected JPanel getContentPanel() {
        return contentPanel;
    }

    public void searchStarted() {
    }

    public void searchFinished() {
        Mutex.EVENT.writeAccess(new Runnable() {
            @Override
            public void run() {
                showRefreshButton();
            }
        });
    }

    /**
     * Set btnStopRefresh to show refresh icon.
     */
    protected void showRefreshButton() {
        btnStopRefresh.setToolTipText(Bundle.ACTION_SearchAgain());
        btnStopRefresh.setIcon(
                ImageUtilities.loadImageIcon(REFRESH_ICON, true));
        btnStopRefreshInRefreshMode = true;
    }

    protected void modifyCriteria() {
    }

    protected void terminateSearch() {
    }

    protected JToolBar getToolBar() {
        return toolBar;
    }

    protected void addButton(AbstractButton button) {
        toolBar.add(button);
    }

    protected void toggleExpand(Node root, boolean expand) {
        if (expand) {
            getOutlineView().expandNode(root);
        }
        for (Node n : root.getChildren().getNodes()) {
            toggleExpand(n, expand);
        }
        if (!expand) {
            getOutlineView().collapseNode(root);
        }
    }

    protected abstract OutlineView getOutlineView();

    private void initActions() {
        ActionMap map = getActionMap();
        map.put("jumpNext", new PrevNextAction(1));                    // NOI18N
        map.put("jumpPrev", new PrevNextAction(-1));                   // NOI18N
    }

    private void initSelectionListeners() {
        getExplorerManager().addPropertyChangeListener(
                new PropertyChangeListener() {
                    @Override
                    public void propertyChange(PropertyChangeEvent evt) {
                        if (evt.getPropertyName().equals(
                                "selectedNodes")) {                     //NOI18N
                            EventQueue.invokeLater(new Runnable() {    //#218680
                                @Override
                                public void run() {
                                    updateShiftButtons();
                                }
                            });
                        }
                    }
                });
    }

    protected void updateShiftButtons() {
        if (btnPrev.isVisible() && btnNext.isVisible()) {
            btnPrev.setEnabled(
                    findShiftNode(-1, getOutlineView(), false) != null);
            btnNext.setEnabled(
                    findShiftNode(1, getOutlineView(), false) != null);
        }
    }

    private void shift(int direction) {

        Node next = findShiftNode(direction, getOutlineView(), true);
        if (next != null) {
            try {
                getExplorerManager().setSelectedNodes(new Node[]{next});
                onDetailShift(next);
            } catch (PropertyVetoException pve) {
                Exceptions.printStackTrace(pve);
            }
        }
    }

    /**
     * Should be called after a matching node was added to update state of
     * buttons.
     */
    protected void afterMatchingNodeAdded() {
        Mutex.EVENT.writeAccess(new Runnable() {
            @Override
            public void run() {
                if (btnNext.isVisible() && !btnNext.isEnabled()) {
                    updateShiftButtons();
                }
            }
        });
    }

    /**
     * Called when a node is selected by clicking Next or Previous button.
     */
    protected void onDetailShift(Node n) {
    }

    private Node findShiftNode(int direction, OutlineView outlineView,
                               boolean canExpand) {
        Node[] selected = getExplorerManager().getSelectedNodes();
        Node n = null;
        if ((selected == null || selected.length == 0)
            /* TODO && getExplorerManager().getRootContext() == resultsOutlineSupport.getRootNode() */) {
            n = getExplorerManager().getRootContext();
        } else if (selected.length == 1) {
            n = selected[0];
        }
        return n == null ? null : findDetailNode(n, direction, outlineView,
                canExpand);
    }

    Node findDetailNode(Node fromNode, int direction,
                        OutlineView outlineView, boolean canExpand) {
        return findUp(fromNode, direction,
                isDetailNode(fromNode) || direction < 0 ? direction : 0,
                outlineView, canExpand);
    }

    /**
     * Start finding for next or previous occurance, from a node or its previous
     * or next sibling of node {@code node}
     *
     * @param node   reference node
     * @param offset 0 to start from node {@code node}, 1 to start from its next
     *               sibling, -1 to start from its previous sibling.
     * @param dir    Direction: 1 for next, -1 for previous.
     */
    Node findUp(Node node, int dir, int offset, OutlineView outlineView,
                boolean canExpand) {
        if (node == null) {
            return null;
        }
        Node parent = node.getParentNode();
        Node[] siblings;
        if (parent == null) {
            siblings = new Node[]{node};
        } else {
            siblings = getChildren(parent, outlineView, canExpand);
        }
        int nodeIndex = findChildIndex(node, siblings);
        if (nodeIndex + offset < 0 || nodeIndex + offset >= siblings.length) {
            return findUp(parent, dir, dir, outlineView, canExpand);
        }
        for (int i = nodeIndex + offset;
             i >= 0 && i < siblings.length; i += dir) {
            Node found = findDown(siblings[i], siblings, i, dir, outlineView,
                    canExpand);
            return found;
        }
        return findUp(parent, dir, offset, outlineView, canExpand);
    }

    /**
     * Find Depth-first search to find a detail node in the subtree.
     */
    private Node findDown(Node node, Node[] siblings, int nodeIndex,
                          int dir, OutlineView outlineView, boolean canExpand) {

        Node[] children = getChildren(node, outlineView, canExpand);
        for (int i = dir > 0 ? 0 : children.length - 1;
             i >= 0 && i < children.length; i += dir) {
            Node found = findDown(children[i], children, i, dir, outlineView,
                    canExpand);
            if (found != null) {
                return found;
            }
        }
        for (int i = nodeIndex; i >= 0 && i < siblings.length; i += dir) {
            if (isDetailNode(siblings[i])) {
                return siblings[i];
            }
        }
        return null;
    }

    protected abstract boolean isDetailNode(Node n);

    private static int findChildIndex(Node selectedNode, Node[] siblings) {
        int pos = -1;
        for (int i = 0; i < siblings.length; i++) {
            if (siblings[i] == selectedNode) {
                pos = i;
                break;
            }
        }
        return pos;
    }

    private static Node[] getChildren(Node n, OutlineView outlineView,
                                      boolean canExpand) {
        if (outlineView != null) {
            if (!outlineView.isExpanded(n)) {
                if (canExpand) {
                    outlineView.expandNode(n);
                } else {
                    return n.getChildren().getNodes(true);
                }
            }
            return getChildrenInDisplayedOrder(n, outlineView);
        } else {
            return n.getChildren().getNodes(true);
        }
    }

    private static Node[] getChildrenInDisplayedOrder(Node parent,
                                                      OutlineView outlineView) {

        Outline outline = outlineView.getOutline();
        Node[] unsortedChildren = parent.getChildren().getNodes(true);
        int rows = outlineView.getOutline().getRowCount();
        int start = findRowIndexInOutline(parent, outline, rows);
        if (start == -1) {
            return unsortedChildren;
        }
        List<Node> children = new LinkedList<Node>();
        for (int j = start + 1; j < rows; j++) {
            int childModelIndex = outline.convertRowIndexToModel(j);
            if (childModelIndex == -1) {
                continue;
            }
            Object childObject = outline.getModel().getValueAt(
                    childModelIndex, 0);
            Node childNode = Visualizer.findNode(childObject);
            if (childNode.getParentNode() == parent) {
                children.add(childNode);
            } else if (children.size() == unsortedChildren.length) {
                break;
            }
        }
        return children.toArray(new Node[children.size()]);
    }

    private static int findRowIndexInOutline(Node node, Outline outline,
                                             int rows) {

        int startRow = Math.max(outline.getSelectedRow(), 0);
        int offset = 0;
        while (startRow + offset < rows || startRow - offset >= 0) {
            int up = startRow + offset + 1;
            int down = startRow - offset;

            if (up < rows && testNodeInRow(outline, node, up)) {
                return up;
            } else if (down >= 0 && testNodeInRow(outline, node, down)) {
                return down;
            } else {
                offset++;
            }
        }
        return -1;
    }

    private static boolean testNodeInRow(Outline outline, Node node, int i) {
        int modelIndex = outline.convertRowIndexToModel(i);
        if (modelIndex != -1) {
            Object o = outline.getModel().getValueAt(modelIndex, 0);
            Node n = Visualizer.findNode(o);
            if (n == node) {
                return true;
            }
        }
        return false;
    }

    private final class PrevNextAction extends javax.swing.AbstractAction {

        private int direction;

        public PrevNextAction(int direction) {
            this.direction = direction;
        }

        @Override
        public void actionPerformed(java.awt.event.ActionEvent actionEvent) {
            shift(direction);
        }
    }

    @Override
    public Lookup getLookup() {
        return lookup;
    }
}
